跳到主要内容

Go 的 HTTP 标准库-客户端

无论是 Get 还是 Post,只要 resp 中的 body 内容非 nil,都需要手动关闭,否则会导致 goroutine 内存泄漏。

发起 GET 请求

func main(){
resp, err := http.Get("http://example.org/get?name=zhangsan&age=23")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

但是如果我们想要把一些参数做成变量而不是直接放到 url 中怎么操作,代码例子如下:

package main

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
)

func main(){
params := url.Values{}
Url, err := url.Parse("http://example.org/get")
if err != nil {
return
}
params.Set("name","zhangsan")
params.Set("age","23")

//如果参数中有中文参数,这个方法会进行URLEncode
Url.RawQuery = params.Encode()
urlPath := Url.String()
fmt.Println(urlPath) // http://example.org/get?name=zhangsan&age=23

resp, err := http.Get(urlPath)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

解析 JSON 类型的返回结果

package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

type result struct {
Args string `json:"args"`
Headers map[string]string `json:"headers"`
Origin string `json:"origin"`
Url string `json:"url"`
}

func main() {
resp, err := http.Get("http://httpbin.org/get")
if err != nil {
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
var res result
_ = json.Unmarshal(body,&res)
fmt.Printf("%#v", res)
}

GET 请求添加请求头

package main

import (
"fmt"
"io/ioutil"
"net/http"
)

func main() {
client := &http.Client{}
req, _ := http.NewRequest("GET","http://alsritter.icu/get",nil)
req.Header.Add("name","alsritter")
req.Header.Add("age","3")
resp, _ := client.Do(req)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf(string(body))
}

请求如下:

{
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Age": "3",
"Host": "httpbin.org",
"Name": "zhaofan",
"User-Agent": "Go-http-client/1.1"
},
"origin": "211.138.20.170, 211.138.20.170",
"url": "https://httpbin.org/get"
}

发送 Post 请求

这种方式

package main

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
)

func main() {
urlValues := url.Values{}
urlValues.Add("name","zhaofan")
urlValues.Add("age","22")
resp, _ := http.PostForm("http://httpbin.org/post",urlValues)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

请求如下:

{
"args": {},
"data": "",
"files": {},
"form": {
"age": "22",
"name": "zhaofan"
},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "19",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Go-http-client/1.1"
},
"json": null,
"origin": "211.138.20.170, 211.138.20.170",
"url": "https://httpbin.org/post"
}

另外一种方式

package main

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)

func main() {
urlValues := url.Values{
"name":{"zhaofan"},
"age":{"23"},
}
reqBody:= urlValues.Encode()
resp, _ := http.Post("http://httpbin.org/post", "text/html",strings.NewReader(reqBody))
body,_:= ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

请求如下:

{
"args": {},
"data": "age=23&name=zhaofan",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "19",
"Content-Type": "text/html",
"Host": "httpbin.org",
"User-Agent": "Go-http-client/1.1"
},
"json": null,
"origin": "211.138.20.170, 211.138.20.170",
"url": "https://httpbin.org/post"
}

发送 JSON 数据的 post 请求

package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

func main() {
client := &http.Client{}
data := make(map[string]interface{})
data["name"] = "zhaofan"
data["age"] = "23"
bytesData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST","http://httpbin.org/post",bytes.NewReader(bytesData))
resp, _ := client.Do(req)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))

}

请求如下:

{
"args": {},
"data": "{\"age\":\"23\",\"name\":\"zhaofan\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "29",
"Host": "httpbin.org",
"User-Agent": "Go-http-client/1.1"
},
"json": {
"age": "23",
"name": "zhaofan"
},
"origin": "211.138.20.170, 211.138.20.170",
"url": "https://httpbin.org/post"
}

不用 client 的 post 请求

package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

func main() {
data := make(map[string]interface{})
data["name"] = "zhaofan"
data["age"] = "23"
bytesData, _ := json.Marshal(data)
resp, _ := http.Post("http://httpbin.org/post","application/json", bytes.NewReader(bytesData))
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

HTTP 请求流式写入 body

发送一个大文件时,可能不能一次传输完成,这种时候就需要使用流来进行传输

在 Go 标准库里的 http client 的 API 是这样的:

http.NewRequest(method, url string, body io.Reader)

body 是通过 io.Reader 接口来传递,并没有暴露一个 io.Writer 接口来提供写入的办法,先来看看正常情况下怎么写入一个body,示例:

buf := bytes.NewBuffer([]byte("hello"))
http.Post("localhost:8099/report","text/pain",buf)

需要先把要写入的数据放在 Buffer 中,放内存缓存着,但是需要写入大量的数据,如果都放内存里肯定要 OOM 了,http client 并没有提供流式写入的方法,大的数据量直接用 Buffer 肯定是不行的

这时可以使用 io.pipe

调用 io.pipe() 方法会返回 Reader 和 Writer 接口实现对象,通过 Writer 写数据,Reader 就可以读到,利用这个特性就可以实现流式的写入,开一个协程来写,然后把 Reader 传递到方法中,就可以实现 http client body 的流式写入了。

随便写个服务端

func main() {
http.HandleFunc("/report", func(rw http.ResponseWriter, r *http.Request) {
// 这个 Fprintf 可以把数据输入到指定的 IO 中
fmt.Fprintf(rw, "hello world")
})

http.ListenAndServe(":8000", nil)
}

重点是这个客户端:

pr, pw := io.Pipe()
// 开协程写入大量数据
go func(){
for i := 0; i < 100; i++ {
w.Write([]byte(fmt.Sprintf("line: %d.", i)))
}
pw.Close()
}()
// 传递Reader
http.Post("http://localhost:8000/report", "text/pain", r)

wireshark 抓包结果

可以发现这些 TCP 发包都是同一个端口

随便点开一个发送到 8000 端口的包,可以看到传输的内容就是客户端发过去的